import selectors

from ..components import info, stuff, brick, box, enemy
from .. import tools, setup
from .. import constants as C
from ..components import player
import pygame
import os
import json


# 游戏关卡类
class Level:
    def __init__(self):
        pass

    # 将初始化方法放入start方法内，使其能够被反复调用，方便不同阶段信息共享
    # 参数game_info为各个阶段传递的关键游戏信息(时间、生命数等)
    # 每次只需更新game_info并调用start方法即可实现数据更新和传递
    def start(self, game_info):
        self.game_info = game_info
        self.finished = False  # 游戏关卡完结标志初始化为False
        self.next = 'game_over'  # 下一阶段调用方法名，None表示无
        self.info = info.Info('level', self.game_info)  # 实例化Info类
        self.load_map_data()  # 载入地图数据
        self.setup_background()  # 设置地图背景
        self.setup_start_positions()  # 初始化角色位置坐标
        self.setup_player()  # 设置游戏角色方法
        self.setup_ground_items()  # 实例化地图元素方法
        self.setup_bricks_and_boxes()  # 实例化地图砖块及宝箱方法(两者几乎一样，因此写在一起)
        self.setup_enemies()  # 实例化地图野怪方法
        self.setup_checkpoints()  # 实例化地图检查点(触发对应批次野怪出现)

    # 载入地图数据
    def load_map_data(self):
        file_name = 'level_1.json'
        file_path = os.path.join('source/data/maps', file_name)
        with open(file_path) as f:
            self.map_data = json.load(f)

    # 设置地图背景
    def setup_background(self):
        self.image_name = self.map_data['image_name']
        self.background = setup.GRAPHICS[self.image_name]  # 背景图片设置
        rect = self.background.get_rect()  # 背景图片矩形框创建
        # 游戏主界面背景参数配置
        self.background = pygame.transform.scale(self.background,
                                                 (int(rect.width * C.BG_MULTI),
                                                  int(rect.height * C.BG_MULTI)))
        self.background_rect = self.background.get_rect()
        self.game_window = setup.SCREEN.get_rect()  # 获得游戏窗口
        # 建立空图层，用以缓存固定的游戏窗口，超过特定限度游戏窗口移动
        self.game_ground = pygame.Surface((self.background_rect.width, self.background_rect.height))

    # 初始化角色位置坐标
    def setup_start_positions(self):
        self.positions = []
        for data in self.map_data['maps']:
            self.positions.append((data['start_x'], data['end_x'], data['player_x'], data['player_y']))
        self.start_x, self.end_x, self.player_x, self.player_y = self.positions[0]

    # 设置游戏角色方法
    def setup_player(self):
        self.player = player.Player('mario')  # 实例化游戏角色'mario'
        self.player.rect.x = self.game_window.x + self.player_x  # 游戏角色x坐标
        self.player.rect.bottom = self.player_y  # 游戏角色底部y坐标

    # 实例化地图元素方法
    def setup_ground_items(self):
        self.ground_items_group = pygame.sprite.Group()  # 定义一个精灵组，可以存放多个精灵对象
        for name in ['ground', 'pipe', 'step']:
            for item in self.map_data[name]:
                # 将实例化的精灵对象放入精灵组
                self.ground_items_group.add(stuff.Item(item['x'], item['y'], item['width'], item['height'], name))

    # 实例化地图砖块及宝箱方法(两者几乎一样，因此写在一起)
    def setup_bricks_and_boxes(self):
        self.brick_group = pygame.sprite.Group()  # 定义砖块精灵组，存放地图砖块信息
        self.box_group = pygame.sprite.Group()  # 定义宝箱精灵组，存放地图宝箱信息
        self.coin_group = pygame.sprite.Group()  # 定义金币精灵组，存放开箱出的金币
        self.powerup_group = pygame.sprite.Group()  # 加成道具组，存放开箱出的加成道具

        # 创建砖块
        if 'brick' in self.map_data:
            for brick_data in self.map_data['brick']:
                x, y = brick_data['x'], brick_data['y']  # 砖块坐标
                brick_type = brick_data['type']  # 砖块类型
                if brick_type == 0:  # 空砖块
                    if 'brick_num' in brick_data:
                        # TODO batch bricks
                        pass
                    else:
                        self.brick_group.add(brick.Brick(x, y, brick_type, None))
                elif brick_type == 1:  # 金币砖块
                    self.brick_group.add(brick.Brick(x, y, brick_type, self.coin_group))
                else:  # 其他加成道具砖块
                    self.brick_group.add(brick.Brick(x, y, brick_type, self.powerup_group))

        # 创建宝箱砖块
        if 'box' in self.map_data:
            for box_data in self.map_data['box']:
                x, y = box_data['x'], box_data['y']  # 宝箱砖块坐标
                box_type = box_data['type']  # 宝箱砖块类型
                # 根据宝箱类型存放不同的奖励精灵组(金币或其他功能性道具)
                if box_type == 1:  # 金币道具宝箱
                    self.box_group.add(box.Box(x, y, box_type, self.coin_group))
                else:  # 其他加成道具宝箱
                    self.box_group.add(box.Box(x, y, box_type, self.powerup_group))

    # 实例化地图野怪方法
    def setup_enemies(self):
        self.shell_group = pygame.sprite.Group()  # 龟壳组:存放被踩过一次变成龟壳状态的乌龟怪
        self.dying_group = pygame.sprite.Group()  # 死亡精灵组:存放当前已死亡的野怪
        self.enemy_group = pygame.sprite.Group()  # 野怪精灵组:存放当前位置应出现的野怪
        self.enemy_group_dict = {}  # 定义敌人字典{敌人组数：敌人精灵组,...,}存放敌人信息
        for enemy_group_data in self.map_data['enemy']:
            group = pygame.sprite.Group()
            # Python 字典(Dictionary) items() 函数以列表返回可遍历的(键, 值) 元组数组。
            # {'key': 'value','1': '1',...}.items() --> [('key', 'value'), ('1', '1'),...]
            for enemy_group_id, enemy_list in enemy_group_data.items():
                for enemy_data in enemy_list:
                    group.add(enemy.create_enemy(enemy_data))
                self.enemy_group_dict[enemy_group_id] = group

    def setup_checkpoints(self):
        self.checkpoint_group = pygame.sprite.Group()
        for item in self.map_data['checkpoint']:
            x, y, w, h = item['x'], item['y'], item['width'], item['height']
            checkpoint_type = item['type']
            # 野怪组次号不是每个检查点都有，因此使用get方法获取，防止没有时报错
            enemy_groupid = item.get('enemy_groupid')
            # 将实例化后的检查点对象添加到检查点精灵组中
            self.checkpoint_group.add(stuff.Checkpoint(x, y, w, h, checkpoint_type, enemy_groupid))

    # 游戏关卡界面更新方法
    def update(self, surface, keys):
        self.current_time = pygame.time.get_ticks()
        self.player.update(keys, self)  # 调用游戏角色类中的update方法根据按键keys更新游戏角色属性
        # 如果角色已经死亡，继续判断死亡时长 超过3秒 则游戏结束并更新数据(防止死亡后无限掉落)
        if self.player.dead:
            if self.current_time - self.player.death_timer > 3000:
                self.finished = True
                self.update_game_info()  # 更新游戏关键数据(生命数，分数等)
        elif self.is_frozen():  # 判断如果场景静止(场景数据不更新,即无需任何处理操作)
            pass
        else:  # 还未死亡
            self.update_player_position()  # 游戏角色位置更新方法
            self.check_if_go_die()  # 判断角色是否死亡方法
            self.check_checkpoints()  # 检查点碰撞检测方法(碰到则触发事件)
            self.update_game_window()  # 游戏窗口跟随方法(采用锁定角色，更新背景的思路)
            self.info.update()  # 界面信息更新(例如金币不断闪烁)
            self.brick_group.update()  # 砖块组加载
            self.box_group.update()  # 宝箱砖块组加载
            # 更新当前野怪精灵组(帧图片切换等信息，达到动态效果)
            self.enemy_group.update(self)  # 将自身作为参数，方便对野怪当前关卡做碰撞检测
            self.dying_group.update(self)  # 更新正常野怪组和死亡野怪组
            self.shell_group.update(self)  # 更新龟壳组
            self.coin_group.update()  # 更新金币道具组
            self.powerup_group.update(self)  # 更新加成道具组

        self.draw(surface)  # 调用关卡绘制方法进行重绘

    # 判断场景是否冻结(按照原著:超级玛丽变身时场景是静止的)
    def is_frozen(self):
        return self.player.state in ['small2big', 'big2small', 'big2fire', 'fire2small']

    # 游戏角色位置更新方法(根据x/y向角色速度更新当前坐标)
    def update_player_position(self):
        # x direction
        self.player.rect.x += self.player.x_vel
        if self.player.rect.x < self.start_x:
            self.player.rect.x = self.start_x
        elif self.player.rect.right > self.end_x:
            self.player.rect.right = self.end_x
        self.check_x_collisions()

        # y direction
        if not self.player.dead:  # 角色还未死亡才会继续y向碰撞检测
            self.player.rect.y += self.player.y_vel
            self.check_y_collisions()

    # x方向碰撞检测
    def check_x_collisions(self):
        # 简单实现方法：利用精灵类中的.spritecollideany(item, group)方法
        # 判断精灵item是否与精灵组group中任一个精灵有碰撞
        # 返回值：返回精灵组中第一个与检测精灵有碰撞的精灵,否则返回空

        # 1.对地形做碰撞检测
        check_group = pygame.sprite.Group(self.ground_items_group, self.brick_group, self.box_group)  # 检查组(砖块,水管等精灵组)
        collided_sprite = pygame.sprite.spritecollideany(self.player, check_group)  # 碰撞检测
        # 碰撞处理
        if collided_sprite:
            self.adjust_player_x(collided_sprite)

        # 2.对怪物做碰撞检测
        # 变身过程会开启伤害免疫(不会继续触发后续对怪物及龟壳的碰撞检测,防止碰撞处理导致异常情况)
        if self.player.hurt_immune:
            return
        enemy = pygame.sprite.spritecollideany(self.player, self.enemy_group)
        if enemy:
            if self.player.big:  # 判断当前为变大形态
                self.player.state = 'big2small'  # 设置状态为big2small(big to small)
                self.player.hurt_immune = True  # 开启伤害免疫(防止在变小过程触发其他干扰情况)
            else:  # 小形态马里奥碰到敌人则直接死亡
                self.player.go_die()  # 角色x向碰到怪物,则游戏角色死亡(直接游戏结束,就不会触发y向检测)

        # 3.对龟壳做碰撞检测
        shell = pygame.sprite.spritecollideany(self.player, self.shell_group)
        if shell:
            # 角色碰到滑动状态的龟壳会被杀死
            if shell.state == 'slide':
                self.player.go_die()
            # 静止状态的龟壳可以被推动,同时变为滑动状态
            else:
                # 从左向右推动龟壳
                if self.player.rect.x < shell.rect.x:
                    shell.x_vel = 10  # 给以x正向速度
                    shell.rect.x += 40  # 向正向移动40个像素
                    shell.direction = 1  # 朝向为1:右(此参数为了区分帧图片朝向)
                # 从右向左推动龟壳(处理同上)
                else:
                    shell.x_vel = -10
                    shell.rect.x -= 40
                    shell.direction = 0
                shell.state = 'slide'  # 龟壳状态变为滑行状态(在上面代码块已经实现,因此enemy类中不做处理)

        # 4.对加成道具做碰撞检测
        powerup = pygame.sprite.spritecollideany(self.player, self.powerup_group)
        if powerup:
            if powerup.name == 'fireball':
                pass  # 除去角色对火球的碰撞处理(防止火球刚出现与角色重叠而被销毁)
            # if powerup.name == 'mushroom':
            if powerup.name == 'fireflower':
                self.player.state = 'big2fire'
                powerup.kill()

    # y方向碰撞检测
    def check_y_collisions(self):
        # 为了对宝箱砖块做区分处理,采用分组检测(地面和水管、砖块、宝箱、敌人)
        ground_item = pygame.sprite.spritecollideany(self.player, self.ground_items_group)
        brick = pygame.sprite.spritecollideany(self.player, self.brick_group)
        box = pygame.sprite.spritecollideany(self.player, self.box_group)
        enemy = pygame.sprite.spritecollideany(self.player, self.enemy_group)

        # 对不同碰撞对象分组处理:
        # 1.先对于同时顶到砖块和宝箱的情况做区分处理(距离谁近就按顶到谁处理,将另一者置为None)
        # 防止先对砖块做碰撞处理,导致无法触发宝箱的碰撞处理
        if brick and box:
            # 计算角色距离两者各自的距离
            to_brick = abs(self.player.rect.centerx - brick.rect.centerx)
            to_box = abs(self.player.rect.centerx - box.rect.centerx)
            if to_brick > to_box:
                brick = None
            else:
                box = None

        if ground_item:
            self.adjust_player_y(ground_item)
        elif brick:
            self.adjust_player_y(brick)
        elif box:
            self.adjust_player_y(box)
        elif enemy:
            # 变身过程会开启伤害免疫(不会继续触发后续对怪物的碰撞检测,防止碰撞处理导致异常情况)
            if self.player.hurt_immune:
                return
            # 将碰到的野怪移出当前野怪精灵组,并放入死亡野怪精灵组
            # 防止在已死亡野怪下落碰到碰撞体时状态变为行走，导致死而复生
            self.enemy_group.remove(enemy)
            if enemy.name == 'koopa':
                self.shell_group.add(enemy)  # 如果是乌龟,先存入龟壳组
            else:
                self.dying_group.add(enemy)
            if self.player.y_vel < 0:
                how = 'bumped'  # 野怪死法1：角色y向速度<0,说明是从下往上顶(顶死)
            else:
                how = 'trampled'  # 死法2：y向速度>=0,说明野怪被角色踩扁(压扁)
                self.player.state = 'jump'  # 踩到野怪后角色变为跳跃状态(根据实际会小跳一下)
                self.player.rect.bottom = enemy.rect.top  # 从怪物顶部出发
                self.player.y_vel = self.player.jump_vel * 0.8  # y向速度为默认跳跃速度的0.8倍
            enemy.go_die(how, 1 if self.player.face_right else -1)  # 怪物死亡(死法不同，处理方法不同)

        self.check_will_fall(self.player)

    # x方向碰撞处理
    def adjust_player_x(self, sprite):
        # 判断相对位置(角色碰物体：左碰右/右碰左)
        if self.player.rect.x < sprite.rect.x:
            self.player.rect.right = sprite.rect.left
        else:
            self.player.rect.left = sprite.rect.right
        self.player.x_vel = 0  # 碰撞后速度变为0

    # y方向碰撞处理
    def adjust_player_y(self, sprite):
        # 判断相对位置(角色碰物体：上碰下(掉落)/下碰上(上升))
        # downwards:
        if self.player.rect.bottom < sprite.rect.bottom:
            self.player.y_vel = 0  # y向速度变为0
            self.player.rect.bottom = sprite.rect.top  # 角色底部坐标等于碰撞体顶部坐标
            self.player.state = 'walk'  # 角色状态变为行走
        # upwards:对于被顶到的对象不同,分别处理(砖块,宝箱)
        else:
            self.player.y_vel = 7  # y向反弹效果(反弹速度定义为 7)
            self.player.rect.top = sprite.rect.bottom  # 角色顶部坐标等于碰撞体底部坐标
            self.player.state = 'fall'  # 角色状态变为下落

            self.is_enemy_on(sprite)  # 判断顶到的对象上方是否有敌人(根据原著效果:可以隔山打牛)

            # 宝箱被顶处理
            if sprite.name == 'box':
                if sprite.state == 'rest':
                    sprite.go_bumped()
            # 砖块被顶处理
            if sprite.name == 'brick':
                if self.player.big and sprite.brick_type == 0:  # 如果角色是变大状态且砖块是普通砖块
                    sprite.smashed(self.dying_group)  # 砖块会被顶碎,触发smashed方法处理
                elif sprite.state == 'rest':
                    sprite.go_bumped()

    # 检测物体上方是否有怪物(方法:假意将被检物体上移一个像素,检测其是否与怪物精灵组产生碰撞)
    def is_enemy_on(self, sprite):
        sprite.rect.y -= 1
        enemy = pygame.sprite.spritecollideany(sprite, self.enemy_group)
        if enemy:
            # 有碰撞则按照怪物被顶死处理
            self.enemy_group.remove(enemy)
            self.dying_group.add(enemy)
            # 根据被顶的方向,确定怪物的飞行方向
            if sprite.rect.centerx > enemy.rect.centerx:
                enemy.go_die('bumped', -1)  # 右顶左,向左飞
            else:
                enemy.go_die('bumped', 1)  # 左顶右,向右飞
        sprite.rect.y += 1  # 还原被检物体位置

    # 是否能下落检测(解决跳到碰撞体上后走到空中不能下落的问题)
    def check_will_fall(self, sprite):
        # 让被检对象下落一个像素判断是否有碰撞，无碰撞且非跳跃状态则下落(排除因跳跃而在空中无碰撞的情况)
        sprite.rect.y += 1
        check_group = pygame.sprite.Group(self.ground_items_group, self.brick_group, self.box_group)
        collided_sprite = pygame.sprite.spritecollideany(sprite, check_group)
        # 下落需要"同时"满足的条件(1.无碰撞;2.非跳跃状态;3.非场景静止(吃蘑菇变身);)
        # 第3点说明:吃蘑菇变身时,防止角色或蘑菇一方下落时吃到蘑菇误判为下落状态 --> 不能变身
        if not collided_sprite and sprite.state != 'jump' and not self.is_frozen():
            sprite.state = 'fall'
        sprite.rect.y -= 1  # 坐标还原

    def check_checkpoints(self):
        checkpoint = pygame.sprite.spritecollideany(self.player, self.checkpoint_group)
        # 判断角色经过检查点
        if checkpoint:
            if checkpoint.checkpoint_type == 0:  # 将对应批次野怪添加到当前野怪精灵组内
                self.enemy_group.add(self.enemy_group_dict[str(checkpoint.enemy_groupid)])
            checkpoint.kill()  # 精灵对象的kill方法:销毁该精灵对象

    # 判断角色是否死亡(1.掉落到屏幕外/)
    def check_if_go_die(self):
        if self.player.rect.y > C.SCREEN_H:
            self.player.go_die()

    # 游戏数据更新方法(更新生命数等关键信息)
    def update_game_info(self):
        # 判断角色死亡则生命数-1
        if self.player.dead:
            self.game_info['lives'] -= 1
        # 生命数为0则下一阶段为game_over阶段，否则为游戏载入阶段
        if self.game_info['lives'] == 0:
            self.next = 'game_over'
        else:
            self.next = 'load_screen'

    # 窗口跟随(采用锁定角色，更新背景的思路),每次只更新1/3窗口，当角色移动超过限度再次更新
    def update_game_window(self):
        third = self.game_window.x + self.game_window.width / 3
        if self.player.x_vel > 0 and self.player.rect.centerx > third \
                and self.game_window.right < self.end_x:
            self.game_window.x += self.player.x_vel
            self.start_x = self.game_window.x

    # 游戏关卡界面绘制方法
    def draw(self, surface):
        self.game_ground.blit(self.background, self.game_window, self.game_window)  # 绘制游戏地图背景到空图层
        self.game_ground.blit(self.player.image, self.player.rect)  # 绘制游戏角色到空图层
        self.coin_group.draw(self.game_ground)  # 绘制金币道具组
        self.powerup_group.draw(self.game_ground)  # 绘制加成道具组
        # ↑先绘制道具组,视觉上在砖块和宝箱内部道具会被遮住(符合原著效果:更像是从砖块里"冒"出来)
        self.brick_group.draw(self.game_ground)  # 绘制砖块组
        self.box_group.draw(self.game_ground)  # 绘制宝箱砖块组
        self.enemy_group.draw(self.game_ground)  # 绘制当前野怪精灵组内的野怪
        self.dying_group.draw(self.game_ground)  # 绘制当前死亡野怪精灵组内的野怪
        self.shell_group.draw(self.game_ground)  # 绘制当前龟壳精灵组内的野怪

        surface.blit(self.game_ground, (0, 0), self.game_window)  # 将绘制好的空图层渲染到游戏主窗口
        self.info.draw(surface)
